from __future__ import absolute_import, print_function, unicode_literals, division
from future_builtins import * #@UnusedWildImport
if __name__ == '__main__':
    __package__ = b'pyson.schema' #@ReservedAssignment
    import pyson.schema #@UnusedImport

import collections
from pyson import types
from .schema_class import Schema, Root, Anything
from .errors import ValidationError, AdaptationError
from .util import get_schemas

class Container(Schema):
    def as_child(self, obj):
        '''
        Modify Schema instance to become a child of self.
        '''

        if isinstance(obj, Schema):
            pass
        elif isinstance(obj, types.Object):
            obj = Object(obj)
        elif isinstance(obj, types.Array):
            if len(obj) == 0:
                obj = Array()
            elif len(obj) == 1:
                obj = Array(obj[0])
            else:
                raise ValueError('ambiguous types for array: %s' % obj)
        else:
            raise TypeError('not a valid schema type: %s' % type(obj))

        if not (obj.parent is self or obj.parent is None):
            raise ValueError("%s already has parent %s" % (obj, obj.parent))
        obj.parent = self
        return obj

class Object(Container, collections.MutableMapping):
    def __init__(self, schema, has_default=False, **kwds):
        '''
        Represents a JSON Object. JSON objects are essentially a dictionary
        in which keys must be strings.
        
        Examples
        --------
        
        >>> schema = Object({ 'book': Str(), 'answer': Str() | Cte(42) })
        >>> schema.validate({'book': 'HHGTTG', 'answer': 12})
        Traceback (most recent call last):
        ...
        ValidationError: $.answer is none of: '<Str>, <Cte>'
        '''

        if has_default:
            raise NotImplementedError

        if not schema:
            raise ValueError('empty schema')

        self._schema = {}
        self._root_fields = []
        self._other_fields = []
        for k, v in schema.items():
            self[k] = self.as_child(v)

        # Compute the order each field was created
        fields = sorted((f._ordering_index, k, f) for (k, f) in self._schema.items())
        self._root_fields = [ k for (_, k, f) in fields if isinstance(f, Root)]
        self._other_fields = [ k for (_, k, f) in fields if not isinstance(f, Root)]
        super(Object, self).__init__(**kwds)

    @property
    def rootsize(self):
        return len(self._root_fields)

    def validate(self, obj, lazy=1, **kwds):
        if not isinstance(obj, types.Object):
            msg = "{path}: not a mapping, got a %s." % type(obj).__name__
            raise ValidationError(msg)

        allkeys = set(obj)
        for key, validator in self.items():
            try:
                value = obj[key]
            except KeyError:
                if not validator.isvalidempty(lazy, **kwds):
                    raise ValidationError('missing path: {path}', [key])
            else:
                allkeys.remove(key)
                try:
                    validator.validate(value, lazy, **kwds)
                except ValidationError as ex:
                    ex.path.insert(0, key)
                    raise

        # Assert obj do not have extra keys
        if lazy < 3 and allkeys:
            key = next(iter(allkeys))
            raise ValidationError('field should not exist: {path}', [key])

    def validateroot(self, obj, **kwds):
        if not self.rootsize:
            super(Object, self).validateroot(obj, **kwds)

        for _, (key, sch) in zip(range(self.rootsize), self.items()):
            try:
                value = obj[key]
            except KeyError:
                raise ValidationError('missing path: {path}', [key])
            else:
                try:
                    sch.validate(value, **kwds)
                except ValidationError as ex:
                    ex.path.insert(0, key)

    def adapt(self, obj, **kwds):
        inplace = kwds.get('inplace', False)
        lazy = kwds.get('lazy', 1)
        try:
            result = super(Object, self).adapt(obj, **kwds)
        except AdaptationError:
            result = kwds.get('makedict', dict)()
            for key, sch in self.items():
                try:
                    adapted = sch.adapt(obj[key], **kwds)
                except KeyError:
                    if lazy >= 2:
                        adapted = sch.default_or_null
                    elif lazy == 1:
                        try:
                            adapted = sch.default
                        except AttributeError:
                            raise AdaptationError('field does not exist: {path}', [key])

                except AdaptationError as ex:
                    ex.path.insert(0, key)
                    raise
                result[key] = adapted

        if inplace and id(result) != id(obj):
            for (k, v) in result.items():
                obj[k] = v
            for k in set(obj) - set(result):
                del obj[k]
        elif not inplace and id(result) == id(obj):
            result = kwds.get('makedict', dict)(obj)

        return result

#    def expand(self, obj, **kwds):
#        expanded = {}
#        expanded.update((unicode(k), v) for (k, v) in obj.items())
#        for key in self:
#            if key not in obj:
#                try:
#                    value = self[key].expand(obj[key], **kwds)
#                except KeyError:
#                    value = self._schemas[key].default_or_null
#                expanded[key] = value
#        return expanded
#
#    def compress(self, obj, **kwds):
#        '''
#        >>> from pyson.schema import *
#        >>> schema = Schema({ u'hello': Str(u'world!'), 'ham?': Str(u'spam') })
#        >>> schema.compress({ u'hello': u'world!', u'ham': u'eggs' })
#        {u'ham': u'eggs'}
#        
#        >>> schema.compress({ u'hello': u'world!' }, use_default=False)
#        {u'hello': u'world!'}
#        '''
#
#        compress_defaults = kwds.get('use_default', True)
#        compressed = {}
#        for k, v in obj.items():
#            sch = self[k]
#            if v == sch.default_or_null:
#                if sch.is_optional or compress_defaults:
#                    continue
#            compressed[unicode(k)] = sch.compress(v, **kwds)
#        return compressed
#
    #===========================================================================
    # Magic methods: support the Mapping protocol
    #===========================================================================
    def __getitem__(self, key):
        return self._schema[key]

    def __setitem__(self, key, value):
        if not isinstance(value, Schema):
            raise TypeError('values must be Schema instances, got %s' % type(value))

        value = self.as_child(value)
        if key in self._schema:
            if isinstance(value, Root) != isinstance(self[key], Root):
                del self[key]
        if key not in self._schema:
            if isinstance(value, Root):
                self._root_fields.append(key)
            else:
                self._other_fields.append(key)
        self._schema[key] = value

    def __delitem__(self, key):
        del self._schema[key]
        if isinstance(key, Root):
            self._root_fields.remove(key)
        else:
            self._other_fields.remove(key)

    def __iter__(self):
        for f in self._root_fields:
            yield f
        for f in self._other_fields:
            yield f

    def __len__(self):
        return len(self._schema)

class Array(Container):
    def __init__(self, array_t=None, *args, **kwds):
        '''
        Validator for list-like objects.
        
        Arguments
        ---------
        
        array_t : Schema
            Type of objects in array
        '''

        self.array_t = self.as_child(array_t if array_t is not None else Anything())
        super(Array, self).__init__(*args, **kwds)

    def validate(self, obj, lazy=1, **kwds):
        if not isinstance(obj, types.Array):
            raise ValidationError("{path}: %s object is not an Array" % type(obj))

        for idx, item in enumerate(obj):
            try:
                self.array_t.validate(item, lazy, **kwds)
            except ValidationError as ex:
                ex.add_subpath(idx)
                raise

#    def _expand_or_compress(self, is_expand, obj, **kwds):
#        # Make a super call and disable validation
#        # This will raise proper errors if obj is not valid and will guarantee
#        # that if obj == null than obj is null. 
#        obj = super(Array, self)._expand_or_compress(is_expand, obj, **kwds)
#        kwds['validate'] = False
#
#        if obj is self.null:
#            return obj
#        else:
#            func = (self.type.expand if is_expand else self.type.compress)
#            for i, item in enumerate(obj):
#                obj[i] = func(item, **kwds)
#            return obj
#
#    def adapt(self, obj, inplace=False, path=[]):
#        if not inplace:
#            obj = list(obj)
#        for idx, v in enumerate(obj):
#            obj[idx] = self.array_t.adapt(v, inplace, path)
#        return obj

    def __getitem__(self, idx):
        if isinstance(idx, int):
            return self.array_t
        else:
            raise TypeError('invalid index of type %s' % type(idx))

# Save specialized _schemas into the Schema class
Schema.OBJECT_SCHEMA = Object
Schema.ARRAY_SCHEMA = Array

__all__ = get_schemas(globals())
if __name__ == '__main__':
    from . import *
#    from pyson.new_schema import *
#    o = Object({'foo': Number(), 'bar': Str()})
#    print(Array(o).isvalid([{'foo': 1, 'bar': 'two'}]))
#
#    obj = Object({'foo': Cte('bar'),
#                  'bar': [Number(2)],
#                  'ham': Optional(Bool(True)),
#                  'name': Root('teste')})
#
#    obj.validate({'foo': 'bar', 'name': 'teste', 'ham': False, 'bar': [1, 2, 'fs']})

    schema = Object({'name': Str(), 'age': Int()})
    print(schema.adapt({'name': 'Arthur', 'age': 31.0}))

    from pyson.schema import *
    import doctest
    doctest.testmod(optionflags=doctest.REPORT_ONLY_FIRST_FAILURE, verbose=0)
